package com.aliothservice.catfish.identity.data;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.web.multipart.MultipartFile;

import com.aliothservice.catfish.admin.data.PlatformRealtimeInfo;
import com.aliothservice.catfish.identity.request.GetCertificateRequest;
import com.aliothservice.catfish.identity.request.SetUserDataRequest;
import com.aliothservice.catfish.identity.request.UpdateEnterpriseCertInfoRequest;
import com.aliothservice.catfish.identity.request.UpdateIndividualCertInfoRequest;
import com.aliothservice.catfish.identity.request.UserDataRequest;
import com.aliothservice.catfish.identity.request.WithdrawRequestRequest;
import com.alipay.config.AlipayConfig;
import com.alipay.util.AlipaySubmit;
import com.mongodb.WriteResult;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSFile;

import net.coobird.thumbnailator.Thumbnails;

enum CertificateFileType {
	INDIVIDUAL_ID_FRONT,
	INDIVIDUAL_ID_BACK,
	INDIVIDUAL_RECENT_PHOTO,
	ENTERPRISE_LICENSE,
	ENTERPRISE_RECENT_PHOTO,
	ENTERPRISE_ID_FRONT,
	ENTERPRISE_ID_BACK
}

public class IdentityDataManager {
	
	public static final int PREFERRED_IMAGE_SIZE = 800;
	public static final int PREFFERED_AVATAR_SIZE = 150;
	
	public User findUser(String phoneNumber) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		User user = mongoTemplate.findOne(query, User.class);
		
		return user;
	}
	
	// precondition - user identified by phoneNumber does not exist!
	public void createUser(String phoneNumber, String password) {
		if (logger.isDebugEnabled()) {
			User user = findUser(phoneNumber);
			if (null != user) {
				logger.debug("trying to create user who has created - " + phoneNumber);
				return;
			}
		}
		
		// create new user
		User user = new User();
		user.setPhoneNumber(phoneNumber);
		user.setPassword(password);
		user.setAccountStatus(User.ACCOUNT_STATUS_CREATED);
		user.setRegistrationDate(new Date());
		mongoTemplate.save(user);
		logger.info("New user has created - " + phoneNumber);
	}
	
	public boolean updatePassword(User user, String newPassword) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(user.getPhoneNumber());
		query.addCriteria(criteria);
		Update update = Update.update("password", newPassword);
		WriteResult result = mongoTemplate.updateFirst(query, update, User.class);
		if (!result.isUpdateOfExisting()) {
			return false;
		}
		
		return true;
	}
	
	public boolean canProceedIndividualVerificationPending(String phoneNumber) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		Criteria inReviewCriteria = Criteria.where("status").is(IndividualPending.STATUS_READY_FOR_REVIEW);
		query.addCriteria(inReviewCriteria);
		
		IndividualPending pending = mongoTemplate.findOne(query, IndividualPending.class);
		if (null == pending) {
			return true;
		} else {
			return false;
		}
	}
	
	public boolean canProceedEnterpriseVerificationPending(String phoneNumber) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		Criteria inReviewCriteria = Criteria.where("status").is(EnterprisePending.STATUS_READY_FOR_REVIEW);
		query.addCriteria(inReviewCriteria);
		
		EnterprisePending pending = mongoTemplate.findOne(query, EnterprisePending.class);
		if (null == pending) {
			return true;
		} else {
			return false;
		}
	}
	
	public IndividualPending findUpsertedIndividualPending(String phoneNumber) {
		Query query = new Query();
		Criteria isCriteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(isCriteria);
		Criteria upsertedCriteria = Criteria.where("status").is(IndividualPending.STATUS_UPSERTED);
		query.addCriteria(upsertedCriteria);
		
		return mongoTemplate.findOne(query, IndividualPending.class);
	}
	
	public IndividualPending findInReviewIndividualPending(String phoneNumber) {
		Query query = new Query();
		Criteria isCriteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(isCriteria);
		Criteria inReviewCriteria = Criteria.where("status").is(IndividualPending.STATUS_READY_FOR_REVIEW);
		query.addCriteria(inReviewCriteria);
		
		return mongoTemplate.findOne(query, IndividualPending.class);
	}
	
	public EnterprisePending findUpsertedEnterprisePending(String phoneNumber) {
		Query query = new Query();
		Criteria isCriteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(isCriteria);
		Criteria upsertedCriteria = Criteria.where("status").is(EnterprisePending.STATUS_UPSERTED);
		query.addCriteria(upsertedCriteria);
		
		return mongoTemplate.findOne(query, EnterprisePending.class);
	}
	
	public EnterprisePending findInReviewEnterprisePending(String phoneNumber) {
		Query query = new Query();
		Criteria isCriteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(isCriteria);
		Criteria inReviewCriteria = Criteria.where("status").is(EnterprisePending.STATUS_READY_FOR_REVIEW);
		query.addCriteria(inReviewCriteria);
		
		return mongoTemplate.findOne(query, EnterprisePending.class);
	}
		
	public boolean upsertIndividualPending(IndividualPending pending, String refId) {
		IndividualPending rejectedPending = null;
		if (null != refId) {
			Query locateQuery = new Query();
			Criteria locateIdCriteria = Criteria.where("id").is(refId);
			Criteria locateUserCriteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
			Criteria locateAndCriteria = new Criteria();
			locateAndCriteria.andOperator(locateIdCriteria, locateUserCriteria);
			locateQuery.addCriteria(locateAndCriteria);
			rejectedPending = mongoTemplate.findOne(locateQuery, IndividualPending.class);
			if (null == rejectedPending) {
				logger.warn("upsertIndividualPending - cannot locate rejected pending by id: " + refId);
				return false;
			}
		}
		
		Query query = new Query();
		Criteria criteria;
		if (null == pending.getId()) {
			criteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
		} else {
			criteria = Criteria.where("id").is(pending.getId());
		}
		query.addCriteria(criteria);
		IndividualPending upsertedPending = mongoTemplate.findOne(query, IndividualPending.class);
		Update update = new Update();
		// assign photos as necessary
		if ((null!=upsertedPending) && (null!=rejectedPending)) {
			if (null == upsertedPending.getFrontImageId()) {
				update.set("frontImageId", rejectedPending.getFrontImageId());
			}
			if (null == upsertedPending.getBackImageId()) {
				update.set("backImageId", rejectedPending.getBackImageId());
			}
			if (null == upsertedPending.getRecentPhotoId()) {
				update.set("recentPhotoId", rejectedPending.getRecentPhotoId());
			}
		}
		update.set("phoneNumber", pending.getPhoneNumber());
		if (null != pending.getLegalName()) {
			update.set("legalName", pending.getLegalName());
		}
		if (null != pending.getProvince()) {
			update.set("province", pending.getProvince());
		}
		if (null != pending.getCity()) {
			update.set("city", pending.getCity());
		}
		if (null != pending.getDistrict()) {
			update.set("district", pending.getDistrict());
		}
		if (null != pending.getAddress()) {
			update.set("address", pending.getAddress());
		}
		if (null != pending.getEmergencyContactName()) {
			update.set("emergencyContactName", pending.getEmergencyContactName());
		}
		if (null != pending.getEmergencyContactPhoneNumber()) {
			update.set("emergencyContactPhoneNumber", pending.getEmergencyContactPhoneNumber());
		}
		update.set("lastUpdateTime", new Date());
		update.set("status", IndividualPending.STATUS_UPSERTED);
		
		WriteResult result = mongoTemplate.upsert(query, update, IndividualPending.class);
		if (1 != result.getN()) {
			return false;
		}
		
		return true;
	}
	
	public boolean updateIndividualCertInfo(String phoneNumber, UpdateIndividualCertInfoRequest req) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		Update update = new Update();
		if (null != req.getEmergencyContactName()) {
			update.set("individualInfo.emergencyContactName", req.getEmergencyContactName());
		}
		if (null != req.getEmergencyContactPhoneNumber()) {
			update.set("individualInfo.emergencyContactPhoneNumber", req.getEmergencyContactPhoneNumber());
		}
		
		WriteResult result = mongoTemplate.updateFirst(query, update, User.class);
		if (1 != result.getN()) {
			return false;
		}
		
		return true;
	}
	
	public boolean updateEnterpriseCertInfo(String phoneNumber, UpdateEnterpriseCertInfoRequest req) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		Update update = new Update();
		if (null != req.getEmergencyContactName()) {
			update.set("enterpriseInfo.contactName", req.getEmergencyContactName());
		}
		if (null != req.getEmergencyContactPhoneNumber()) {
			update.set("enterpriseInfo.contactPhoneNumber", req.getEmergencyContactPhoneNumber());
		}
		
		WriteResult result = mongoTemplate.updateFirst(query, update, User.class);
		if (!result.isUpdateOfExisting()) {
			return false;
		}
		
		return true;
	}
	
	public boolean upsertEnterprisePending(EnterprisePending pending, String refId) {
		EnterprisePending rejectedPending = null;
		if (null != refId) {
			Query locateQuery = new Query();
			Criteria locateIdCriteria = Criteria.where("id").is(refId);
			Criteria locateUserCriteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
			Criteria locateAndCriteria = new Criteria();
			locateAndCriteria.andOperator(locateIdCriteria, locateUserCriteria);
			locateQuery.addCriteria(locateAndCriteria);
			rejectedPending = mongoTemplate.findOne(locateQuery, EnterprisePending.class);
			if (null == rejectedPending) {
				logger.warn("upsertIndividualPending - cannot locate rejected pending by id: " + refId);
				return false;
			}
		}
		
		Query query = new Query();
		Criteria criteria;
		if (null == pending.getId()) {
			criteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
		} else {
			criteria = Criteria.where("id").is(pending.getId());
		}
		query.addCriteria(criteria);
		EnterprisePending upsertedPending = mongoTemplate.findOne(query, EnterprisePending.class);
		Update update = new Update();
		// assign photos as necessary
		if ((null!=upsertedPending) && (null!=rejectedPending)) {
			if (null == upsertedPending.getFrontImageId()) {
				update.set("frontImageId", rejectedPending.getFrontImageId());
			}
			if (null == upsertedPending.getBackImageId()) {
				update.set("backImageId", rejectedPending.getBackImageId());
			}
			if (null == upsertedPending.getRecentPhotoId()) {
				update.set("recentPhotoId", rejectedPending.getRecentPhotoId());
			}
			if (null == upsertedPending.getLicenseImageId()) {
				update.set("licenseImageId", rejectedPending.getLicenseImageId());
			}
		}
		update.set("phoneNumber", pending.getPhoneNumber());
		if (null != pending.getEnterpriseName()) {
			update.set("enterpriseName", pending.getEnterpriseName());
		}
		if (null != pending.getProvince()) {
			update.set("province", pending.getProvince());
		}
		if (null != pending.getCity()) {
			update.set("city", pending.getCity());
		}
		if (null != pending.getDistrict()) {
			update.set("district", pending.getDistrict());
		}
		if (null != pending.getAddress()) {
			update.set("address", pending.getAddress());
		}
		if (null != pending.getContactName()) {
			update.set("contactName", pending.getContactName());
		}
		if (null != pending.getContactPhoneNumber()) {
			update.set("contactPhoneNumber", pending.getContactPhoneNumber());
		}
		update.set("lastUpdateTime", new Date());
		update.set("status", EnterprisePending.STATUS_UPSERTED);
		
		WriteResult result = mongoTemplate.upsert(query, update, EnterprisePending.class);
		if (1 != result.getN()) {
			return false;
		}
		
		return true;
	}
	
	public boolean completeIndividualPending(IndividualPending pending) {
		Query query = new Query();
		Criteria criteria = Criteria.where("id").is(pending.getId());
		query.addCriteria(criteria);
		Update update = new Update();
		update.set("lastUpdateTime", new Date());
		update.set("status", IndividualPending.STATUS_READY_FOR_REVIEW);
		
		WriteResult result = mongoTemplate.updateFirst(query, update, IndividualPending.class);
		if (1 != result.getN()) {
			return false;
		}
		
		// remove rejected individual info as necessary
		Query rejectedQuery  = new Query();
		Criteria rejectedUserCriteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
		Criteria rejectedInfoCriteria = Criteria.where("rejectedIndividualInfo").exists(true);
		Criteria rejectedAndCriteria = new Criteria();
		rejectedAndCriteria.andOperator(rejectedUserCriteria, rejectedInfoCriteria);
		rejectedQuery.addCriteria(rejectedAndCriteria);
		Update rejectedUpdate = new Update();
		rejectedUpdate.unset("rejectedIndividualInfo");
		WriteResult rejectedResult = mongoTemplate.updateFirst(rejectedQuery, rejectedUpdate, User.class);
		if (rejectedResult.isUpdateOfExisting()) {
			logger.info("completeIndividualPending - clears rejected info from user object");
		}
		
		// generate real time info
		PlatformRealtimeInfo realtimeInfo = new PlatformRealtimeInfo();
		realtimeInfo.setModule(PlatformRealtimeInfo.MODULE_USER);
		realtimeInfo.setIdentifier(PlatformRealtimeInfo.IDENTIFIER_INDIVIDUAL);
		realtimeInfo.setIndividualName(pending.getLegalName());
		realtimeInfo.setIndividualLocation(pending.getProvince()+pending.getCity()+pending.getDistrict());
		realtimeInfo.setIndividualContactName(pending.getEmergencyContactName());
		realtimeInfo.setIndividualContactPhoneNumber(pending.getEmergencyContactPhoneNumber());
		realtimeInfo.setCreationDate(LocalDateTime.now());
		mongoTemplate.save(realtimeInfo);
		
		return true;
	}
	
	public boolean completeEnterprisePending(EnterprisePending pending) {
		Query query = new Query();
		Criteria criteria = Criteria.where("id").is(pending.getId());
		query.addCriteria(criteria);
		Update update = new Update();
		update.set("lastUpdateTime", new Date());
		update.set("status", EnterprisePending.STATUS_READY_FOR_REVIEW);
		
		WriteResult result = mongoTemplate.updateFirst(query, update, EnterprisePending.class);
		if (1 != result.getN()) {
			return false;
		}
		
		// remove rejected enterprise info as necessary
		Query rejectedQuery  = new Query();
		Criteria rejectedUserCriteria = Criteria.where("phoneNumber").is(pending.getPhoneNumber());
		Criteria rejectedInfoCriteria = Criteria.where("rejectedEnterpriseInfo").exists(true);
		Criteria rejectedAndCriteria = new Criteria();
		rejectedAndCriteria.andOperator(rejectedUserCriteria, rejectedInfoCriteria);
		rejectedQuery.addCriteria(rejectedAndCriteria);
		Update rejectedUpdate = new Update();
		rejectedUpdate.unset("rejectedEnterpriseInfo");
		WriteResult rejectedResult = mongoTemplate.updateFirst(rejectedQuery, rejectedUpdate, User.class);
		if (rejectedResult.isUpdateOfExisting()) {
			logger.info("completeEnterprisePending - clears rejected info from user object");
		}
		
		// generate real time info
		PlatformRealtimeInfo realtimeInfo = new PlatformRealtimeInfo();
		realtimeInfo.setModule(PlatformRealtimeInfo.MODULE_USER);
		realtimeInfo.setIdentifier(PlatformRealtimeInfo.IDENTIFIER_ENTERPRISE);
		realtimeInfo.setEnterpriseName(pending.getEnterpriseName());
		realtimeInfo.setEnterpriseLocation(pending.getProvince()+pending.getCity()+pending.getDistrict());
		realtimeInfo.setEnterpriseContactName(pending.getContactName());
		realtimeInfo.setEnterpriseContactPhoneNumber(pending.getContactPhoneNumber());
		realtimeInfo.setCreationDate(LocalDateTime.now());
		mongoTemplate.save(realtimeInfo);
		
		return true;
	}
	
	public String updateAvatar(String phoneNumber, MultipartFile file) throws IOException {
		String filename = phoneNumber + "_avatar";
		// store the file
		boolean shouldStore = false;
		BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
		if (logger.isDebugEnabled()) {
			logger.debug("processing image <filename:" + filename + ">, width:" + bufferedImage.getWidth() + ", height:" + bufferedImage.getHeight());
		}
		BufferedImage scaledBufferedImage = null;
		if (bufferedImage.getWidth() == bufferedImage.getHeight()) {
			// scale image
			if ((bufferedImage.getWidth()>PREFERRED_IMAGE_SIZE) || (bufferedImage.getHeight()>PREFERRED_IMAGE_SIZE)) {
				scaledBufferedImage = Thumbnails.of(bufferedImage).size(PREFERRED_IMAGE_SIZE, PREFERRED_IMAGE_SIZE).asBufferedImage();
				logger.debug("processing image <filename:" + filename + ">, scaled width:" + scaledBufferedImage.getWidth() + ", scaled height:" + scaledBufferedImage.getHeight());
			}
			shouldStore = true;
		} else {
			// resize and store original image and will be cropped by client application later
			if ((bufferedImage.getWidth()>PREFERRED_IMAGE_SIZE) && (bufferedImage.getHeight()>PREFERRED_IMAGE_SIZE)) {
				int scaleFactor;
				if (bufferedImage.getWidth() > bufferedImage.getHeight()) {
					scaleFactor = bufferedImage.getWidth()*PREFERRED_IMAGE_SIZE/bufferedImage.getHeight();
				} else {
					scaleFactor = bufferedImage.getHeight()*PREFERRED_IMAGE_SIZE/bufferedImage.getWidth();
				}
				scaledBufferedImage = Thumbnails.of(bufferedImage).size(scaleFactor, scaleFactor).asBufferedImage();
				logger.debug("resized original image, width:" + scaledBufferedImage.getWidth() + " height:" + scaledBufferedImage.getHeight());
			}
		}
		GridFSFile storedFile = null;
		if (null != scaledBufferedImage) {
			logger.debug("store scaled image");
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Thumbnails.of(scaledBufferedImage).scale(1.0).outputFormat("jpg").toOutputStream(outputStream);
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
			storedFile = gridFsTemplate.store(inputStream, filename);
		} else {
			logger.debug("store original image");
			storedFile = gridFsTemplate.store(file.getInputStream(), filename);
		}
		if (null == storedFile) {
			return null;
		}
		
		if (shouldStore) {
			Query userQuery = new Query();
			Criteria idCriteria = Criteria.where("phoneNumber").is(phoneNumber);
			userQuery.addCriteria(idCriteria);
			Update userUpdate = new Update();
			userUpdate.set("avatarId", storedFile.getId().toString());
			WriteResult userResult = mongoTemplate.updateFirst(userQuery, userUpdate, User.class);
			if (!userResult.isUpdateOfExisting()) {
				logger.error("updateAvatar - failed to update avatar for " + phoneNumber);
				return null;
			}
		}
		
		if (bufferedImage.getWidth() == bufferedImage.getHeight()) {
			return storedFile.getId().toString();
		} else {
			if (null != scaledBufferedImage) {
				// resized
				return storedFile.getId().toString() + ":" + scaledBufferedImage.getWidth() + ":" + scaledBufferedImage.getHeight();
			} else {
				return storedFile.getId().toString() + ":" + bufferedImage.getWidth() + ":" + bufferedImage.getHeight();
			}
		}
	}
	
	public String cropAvatar(String phoneNumber, String avatarId, String crop) throws IOException {
		String cropExp[] = crop.split(":");
		int posX = Integer.parseInt(cropExp[0]);
		int posY = Integer.parseInt(cropExp[1]);
		int size = Integer.parseInt(cropExp[2]);
		logger.info("cropAvatar - x:" + posX + " y:" + posY + " size:" + size);
		
		// locate original image
		Query query = new Query();
		Criteria criteria = Criteria.where("_id").is(avatarId);
		query.addCriteria(criteria);
		GridFSDBFile file = gridFsTemplate.findOne(query);
		if (null == file) {
			logger.warn("cropAvatar - cannot locate image by id: " + avatarId);
			return null;
		}
		BufferedImage bufferedImage = Thumbnails.of(file.getInputStream()).sourceRegion(posX, posY, size, size).size(size, size).asBufferedImage();
		if (logger.isDebugEnabled()) {
			logger.debug("processing cropped image width:" + bufferedImage.getWidth() + ", height:" + bufferedImage.getHeight());
		}
		BufferedImage scaledBufferedImage = null;
		// scale image
		if ((bufferedImage.getWidth()>PREFFERED_AVATAR_SIZE) || (bufferedImage.getHeight()>PREFFERED_AVATAR_SIZE)) {
			scaledBufferedImage = Thumbnails.of(bufferedImage).size(PREFFERED_AVATAR_SIZE, PREFFERED_AVATAR_SIZE).asBufferedImage();
			logger.debug("processing cropped image, scaled width:" + scaledBufferedImage.getWidth() + ", scaled height:" + scaledBufferedImage.getHeight());
		}
		// store image
		GridFSFile storedFile = null;
		if (null != scaledBufferedImage) {
			logger.debug("store scaled image");
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Thumbnails.of(scaledBufferedImage).scale(1.0).outputFormat("jpg").toOutputStream(outputStream);
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
			storedFile = gridFsTemplate.store(inputStream, phoneNumber+"_avatar");
		} else {
			logger.debug("store cropped image");
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Thumbnails.of(bufferedImage).scale(1.0).outputFormat("jpg").toOutputStream(outputStream);
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
			storedFile = gridFsTemplate.store(inputStream, phoneNumber+"_avatar");
		}
		if (null == storedFile) {
			return null;
		}
		
		// update user
		Query userQuery = new Query();
		Criteria idCriteria = Criteria.where("phoneNumber").is(phoneNumber);
		userQuery.addCriteria(idCriteria);
		Update userUpdate = new Update();
		userUpdate.set("avatarId", storedFile.getId().toString());
		WriteResult userResult = mongoTemplate.updateFirst(userQuery, userUpdate, User.class);
		if (!userResult.isUpdateOfExisting()) {
			logger.error("cropAvatar - failed to update avatar for " + phoneNumber);
			return null;
		}
		
		// clean up
		Query deleteQuery = new Query();
		Criteria deleteCriteria = Criteria.where("_id").is(avatarId);
		deleteQuery.addCriteria(deleteCriteria);
		gridFsTemplate.delete(deleteQuery);
		
		return storedFile.getId().toString();
	}
	
	public String updateIndividualCertificate(String phoneNumber, String type, MultipartFile file) throws IOException {
		
		String fileName = phoneNumber+"_"+type;
		CertificateFileType fileType;
		if (0 == type.compareTo("idfront")) {
			fileType = CertificateFileType.INDIVIDUAL_ID_FRONT;
		} else if (0 == type.compareTo("idback")) {
			fileType = CertificateFileType.INDIVIDUAL_ID_BACK;
		} else {
			// fall back to recent photo
			fileType = CertificateFileType.INDIVIDUAL_RECENT_PHOTO;
		}
		
		// remove unlinked image file
		IndividualPending pending = findUpsertedIndividualPending(phoneNumber);
		if (null != pending) {
			switch (fileType) {
				case INDIVIDUAL_ID_FRONT:
					if (null != pending.getFrontImageId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getFrontImageId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getFrontImageId());
						}
					}
					break;
				case INDIVIDUAL_ID_BACK:
					if (null != pending.getBackImageId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getBackImageId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getBackImageId());
						}
					}
					break;
				case INDIVIDUAL_RECENT_PHOTO:
					if (null != pending.getRecentPhotoId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getRecentPhotoId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getRecentPhotoId());
						}
					}
					break;
				default:
					break;
			}
		}
		
		BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
		if (logger.isDebugEnabled()) {
			logger.debug("processing image <filename:" + fileName + ">, width:" + bufferedImage.getWidth() + ", height:" + bufferedImage.getHeight());
		}
		BufferedImage scaledBufferedImage = null;
		if ((bufferedImage.getWidth()>PREFERRED_IMAGE_SIZE) || (bufferedImage.getHeight()>PREFERRED_IMAGE_SIZE)) {
			scaledBufferedImage = Thumbnails.of(bufferedImage).size(PREFERRED_IMAGE_SIZE, PREFERRED_IMAGE_SIZE).asBufferedImage();
			logger.debug("processing image <filename:" + fileName + ">, scaled width:" + scaledBufferedImage.getWidth() + ", scaled height:" + scaledBufferedImage.getHeight());
		}
		GridFSFile storedFile = null;
		if (null != scaledBufferedImage) {
			logger.debug("store scaled image");
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Thumbnails.of(scaledBufferedImage).scale(1.0).outputFormat("jpg").toOutputStream(outputStream);
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
			storedFile = gridFsTemplate.store(inputStream, fileName);
		} else {
			logger.debug("store original image");
			storedFile = gridFsTemplate.store(file.getInputStream(), fileName);
		}
		if (null == storedFile) {
			return null;
		}
		
		// update IndividualPending
		if (null != pending) {
			Query query = new Query();
			Criteria criteria = Criteria.where("id").is(pending.getId());
			query.addCriteria(criteria);
			Update update = new Update();
			switch (fileType) {
				case INDIVIDUAL_ID_FRONT:
					update.set("frontImageId", storedFile.getId().toString());
					break;
				case INDIVIDUAL_ID_BACK:
					update.set("backImageId", storedFile.getId().toString());
					break;
				case INDIVIDUAL_RECENT_PHOTO:
					update.set("recentPhotoId", storedFile.getId().toString());
					break;
				default:
					break;
			}
			update.set("lastUpdateTime", new Date());
			
			WriteResult result = mongoTemplate.updateFirst(query, update, IndividualPending.class);
			if (1 != result.getN()) {
				return null;
			}
		} else {
			pending = new IndividualPending();
			switch (fileType) {
				case INDIVIDUAL_ID_FRONT:
					pending.setFrontImageId(storedFile.getId().toString());
					break;
				case INDIVIDUAL_ID_BACK:
					pending.setBackImageId(storedFile.getId().toString());
					break;
				case INDIVIDUAL_RECENT_PHOTO:
					pending.setRecentPhotoId(storedFile.getId().toString());
					break;
				default:
					break;
			}
			pending.setPhoneNumber(phoneNumber);
			pending.setLastUpdateTime(new Date());
			pending.setStatus(IndividualPending.STATUS_UPSERTED);
			mongoTemplate.save(pending);
		}
		
		return storedFile.getId().toString();
	}
	
	public String updateEnterpriseCertificate(String phoneNumber, String type, MultipartFile file) throws IOException {
		String filename = phoneNumber + "" + type;
		CertificateFileType fileType;
		if (0 == type.compareTo("license")) {
			fileType = CertificateFileType.ENTERPRISE_LICENSE;
		} else if (0 == type.compareTo("eidfront")) {
			fileType = CertificateFileType.ENTERPRISE_ID_FRONT;
		} else if (0 == type.compareTo("eidback")) {
			fileType = CertificateFileType.ENTERPRISE_ID_BACK;
		} else {
			// fall back to recent photo
			fileType = CertificateFileType.ENTERPRISE_RECENT_PHOTO;
		}
		
		// remove unlinked image file
		EnterprisePending pending = findUpsertedEnterprisePending(phoneNumber);
		if (null != pending) {
			switch (fileType) {
				case ENTERPRISE_LICENSE:
					if (null != pending.getLicenseImageId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getLicenseImageId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getLicenseImageId());
						}
					}
					break;
				case ENTERPRISE_ID_FRONT:
					if (null != pending.getFrontImageId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getFrontImageId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getFrontImageId());
						}
					}
					break;
				case ENTERPRISE_ID_BACK:
					if (null != pending.getBackImageId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getBackImageId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getBackImageId());
						}
					}
					break;
				case ENTERPRISE_RECENT_PHOTO:
					if (null != pending.getRecentPhotoId()) {
						Query deleteQuery = new Query();
						Criteria deleteCriteria = Criteria.where("_id").is(pending.getRecentPhotoId());
						deleteQuery.addCriteria(deleteCriteria);
						gridFsTemplate.delete(deleteQuery);
						if (logger.isDebugEnabled()) {
							logger.debug("removed unlinked image file - id " + pending.getRecentPhotoId());
						}
					}
					break;
				default:
					break;
			}
		}
		
		BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
		if (logger.isDebugEnabled()) {
			logger.debug("processing image <filename:" + filename + ">, width:" + bufferedImage.getWidth() + ", height:" + bufferedImage.getHeight());
		}
		BufferedImage scaledBufferedImage = null;
		if ((bufferedImage.getWidth()>PREFERRED_IMAGE_SIZE) || (bufferedImage.getHeight()>PREFERRED_IMAGE_SIZE)) {
			scaledBufferedImage = Thumbnails.of(bufferedImage).size(PREFERRED_IMAGE_SIZE, PREFERRED_IMAGE_SIZE).asBufferedImage();
			logger.debug("processing image <filename:" + filename + ">, scaled width:" + scaledBufferedImage.getWidth() + ", scaled height:" + scaledBufferedImage.getHeight());
		}
		GridFSFile storedFile = null;
		if (null != scaledBufferedImage) {
			logger.debug("store scaled image");
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Thumbnails.of(scaledBufferedImage).scale(1.0).outputFormat("jpg").toOutputStream(outputStream);
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
			storedFile = gridFsTemplate.store(inputStream, filename);
		} else {
			logger.debug("store original image");
			storedFile = gridFsTemplate.store(file.getInputStream(), filename);
		}
		if (null == storedFile) {
			return null;
		}
		
		// update EnterprisePending
		if (null != pending) {
			Query query = new Query();
			Criteria criteria = Criteria.where("id").is(pending.getId());
			query.addCriteria(criteria);
			Update update = new Update();
			switch (fileType) {
				case ENTERPRISE_LICENSE:
					update.set("licenseImageId", storedFile.getId().toString());
					break;
				case ENTERPRISE_ID_FRONT:
					update.set("frontImageId", storedFile.getId().toString());
					break;
				case ENTERPRISE_ID_BACK:
					update.set("backImageId", storedFile.getId().toString());
					break;
				case ENTERPRISE_RECENT_PHOTO:
					update.set("recentPhotoId", storedFile.getId().toString());
					break;
				default:
					break;
			}
			update.set("lastUpdateTime", new Date());
			
			WriteResult result = mongoTemplate.updateFirst(query, update, EnterprisePending.class);
			if (1 != result.getN()) {
				return null;
			}
		} else {
			pending = new EnterprisePending();
			pending.setPhoneNumber(phoneNumber);
			switch (fileType) {
			case ENTERPRISE_LICENSE:
				pending.setLicenseImageId(storedFile.getId().toString());
				break;
			case ENTERPRISE_ID_FRONT:
				pending.setFrontImageId(storedFile.getId().toString());
				break;
			case ENTERPRISE_ID_BACK:
				pending.setBackImageId(storedFile.getId().toString());
				break;
			case ENTERPRISE_RECENT_PHOTO:
				pending.setRecentPhotoId(storedFile.getId().toString());
				break;
			default:
				break;
			}
			pending.setLastUpdateTime(new Date());
			pending.setStatus(EnterprisePending.STATUS_UPSERTED);
			mongoTemplate.save(pending);
		}
		
		return storedFile.getId().toString();
	}
	
	public GridFSDBFile getCertificate(GetCertificateRequest req) {
		Query query = new Query();
		Criteria criteria = Criteria.where("_id").is(req.getImageId());
		query.addCriteria(criteria);
		
		return gridFsTemplate.findOne(query);
	}
	
	public boolean rejectIndividualPending(String pendingId, String descriptionOfReject) {
		Query query = new Query();
		Criteria criteria = Criteria.where("id").is(pendingId);
		query.addCriteria(criteria);
		Update update = new Update();
		update.set("status", IndividualPending.STATUS_REJECTED);
		update.set("descriptionOfReject", descriptionOfReject);
		
		WriteResult result = mongoTemplate.updateFirst(query, update, IndividualPending.class);
		if (1 != result.getN()) {
			return false;
		}
		
		// TODO post reject notification
		
		return true;
	}
	
	public Map<String, String> accountBalance(String phoneNumber) {
		Map<String, String> result = new HashMap<String, String>();
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		User user = mongoTemplate.findOne(query, User.class);
		if (null == user) {
			result.put("balance", "0");
			result.put("withdrawBalance", "0");
			return result;
		} else {
			if (null == user.getBalance()) {
				result.put("balance", "0");
			} else {
				result.put("balance", user.getBalance().toString());
			}
			if (null == user.getWithdrawBalance()) {
				result.put("withdrawBalance", "0");
			} else {
				result.put("withdrawBalance", user.getWithdrawBalance().toString());
			}
			if (null == user.getWithdrawStatus()) {
				result.put("withdrawStatus", User.WITHDRAW_STATUS_OPEN_FOR_WITHDRAW);
			} else {
				result.put("withdrawStatus", user.getWithdrawStatus());
			}
		}
		
		return result;
	}
	
	public boolean setUserData(String phoneNumber, SetUserDataRequest req) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		User user = mongoTemplate.findOne(query, User.class);
		if (null == user) {
			logger.warn("setUserData - user not found, unable to set user data for key: " + req.getKey() + " and value: " + req.getValue());
			return false;
		}
		Map<String, String> data = user.getData();
		if (null == data) {
			data = new HashMap<String, String>();
		}
		data.put(req.getKey(), req.getValue());
		Update update = new Update();
		update.set("data", data);
		if (!mongoTemplate.updateFirst(query, update, User.class).isUpdateOfExisting()) {
			logger.warn("setUserData - unable to set user data for key: " + req.getKey() + " and value: " + req.getValue());
			return false;
		}
		
		return true;
	}
	
	public String userData(String phoneNumber, UserDataRequest req) {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		User user = mongoTemplate.findOne(query, User.class);
		if (null == user) {
			logger.warn("userData - user not found");
			return null;
		}
		if (null == user.getData()) {
			logger.warn("user Data - data not set");
			return null;
		}
		
		return user.getData().get(req.getKey());
	}
	
	public boolean withdrawRequest(String phoneNumber, WithdrawRequestRequest req) throws UnsupportedEncodingException {
		Query query = new Query();
		Criteria criteria = Criteria.where("phoneNumber").is(phoneNumber);
		query.addCriteria(criteria);
		
		User user = mongoTemplate.findOne(query, User.class);
		if (null == user) {
			logger.warn("withdrawRequest - user not found: " + phoneNumber);
			return false;
		}
		if (null == user.getWithdrawBalance()) {
			logger.warn("withdrawRequest - withdrawBalance not set, invalid request:" + phoneNumber);
			return false;
		}
		if (req.getWithdrawAmount().intValue() > user.getWithdrawBalance().intValue()) {
			logger.warn("withdrawRequest - withdraw amount is not valid, too big, user:" + phoneNumber);
			return false;
		}
		if (null != user.getWithdrawStatus()) {
			if (0 != user.getWithdrawStatus().compareTo(User.WITHDRAW_STATUS_OPEN_FOR_WITHDRAW)) {
				logger.warn("withdrawRequest - multiple withdraw request is not allowed: " + phoneNumber);
				return false;
			}
		}
		
		// create withdraw request first
		WithdrawRequest request = new WithdrawRequest();
		request.setUserId(phoneNumber);
		request.setAlipayAccount(req.getAlipayAccount());
		request.setAlipayUserName(req.getAlipayUserName());
		request.setWithdrawAmount(req.getWithdrawAmount());
		request.setStatus(WithdrawRequest.STATUS_REQUESTED);
		request.setCreationDate(LocalDateTime.now());
		mongoTemplate.save(request);
		Map<String, String> alipayReqMap = new HashMap<String, String>();
		alipayReqMap.put("service", "batch_trans_notify");
		alipayReqMap.put("partner", AlipayConfig.partner);
		alipayReqMap.put("_input_charset", AlipayConfig.input_charset);
		alipayReqMap.put("notify_url", AlipayConfig.withdraw_notify_url);
		alipayReqMap.put("account_name", new String("江苏云达冷链科技有限公司".getBytes(), "UTF-8"));
		alipayReqMap.put("detail_data", "1^"+req.getAlipayAccount()+"^"+req.getAlipayUserName()+"^"+req.getWithdrawAmount().toString()+"^用户提现");
		alipayReqMap.put("batch_no", request.getId());
		alipayReqMap.put("batch_num", "1");
		alipayReqMap.put("batch_fee", req.getWithdrawAmount().toString());
		alipayReqMap.put("email", "yunda@jsyunda.com");
		LocalDateTime now = LocalDateTime.now();
		DateTimeFormatter fullFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
		alipayReqMap.put("pay_date", fullFormatter.format(now));
		String processUrl = AlipaySubmit.buildGetRequest(alipayReqMap);
		// update withdraw request
		Query requestQuery = new Query();
		Criteria requestIdCriteria = Criteria.where("id").is(request.getId());
		requestQuery.addCriteria(requestIdCriteria);
		Update requestUpdate = new Update();
		requestUpdate.set("processUrl", processUrl);
		if (!mongoTemplate.updateFirst(requestQuery, requestUpdate, WithdrawRequest.class).isUpdateOfExisting()) {
			logger.error("withdrawRequest - failed to update withdraw request just created??");
			return false;
		}
		
		Update update = new Update();
		update.set("withdrawStatus", User.WITHDRAW_STATUS_IN_PROGRESS);
		if (!mongoTemplate.updateFirst(query, update, User.class).isUpdateOfExisting()) {
			logger.error("withdrawRequest - failed to update withdraw status:" + phoneNumber);
		}
		
		return true;
	}
	
	public IdentityDataManager() {
		
	}
	
	@Autowired
	private MongoTemplate mongoTemplate;
	
	@Autowired
	private GridFsTemplate gridFsTemplate;
	
	private static final Logger logger = LoggerFactory.getLogger(IdentityDataManager.class);
	
}
